<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第九章：实际应用案例 - 9.6 构建一个基于RAG的问答机器人</title>
    <link rel="stylesheet" href="../shared_styles.css">
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script>
        function renderMarkdown(markdownText, elementId) {
            document.getElementById(elementId).innerHTML = marked.parse(markdownText);
        }
    </script>
  
</head>
<body>
    <div class="container">
        <header>
            <h1>第九章：实际应用案例</h1>
        </header>

        <main>
            <section id="s9-6">
                <h2>9.6 构建一个基于检索增强生成 (RAG) 的问答机器人</h2>
                <p>本节将详细介绍如何构建一个基于检索增强生成 (Retrieval Augmented Generation, RAG) 的问答机器人。该机器人能够根据您提供的特定文档内容来回答问题，而不是仅仅依赖模型预训练时的通用知识。</p>

                <div id="rag-chatbot-content">
                    <p>检索增强生成 (RAG) 是一种强大的技术，它将大型语言模型 (LLM) 的生成能力与外部知识库的检索能力相结合。对于问答机器人而言，这意味着机器人可以基于您提供的特定文档、数据库或实时信息源来回答问题，而不仅仅依赖于其预训练数据中的通用知识。这使得答案更加准确、相关，并且能够处理特定领域或最新信息的问题。</p>

                    <h3>为什么选择RAG构建问答机器人？</h3>
                    <ul>
                        <li><strong>提高答案的准确性和相关性：</strong> 通过从特定知识源检索信息，RAG可以提供更精确、与上下文更相关的答案。</li>
                        <li><strong>减少幻觉：</strong> LLM有时会“编造”信息（称为幻觉）。RAG通过将答案植根于检索到的文档，可以显著减少这种情况。</li>
                        <li><strong>处理领域特定知识：</strong> 通用LLM可能不具备特定行业或组织的专业知识。RAG允许您为LLM“注入”这些特定知识。</li>
                        <li><strong>知识更新的灵活性：</strong> 当外部知识库更新时，RAG系统可以立即利用这些新信息，而无需重新训练整个LLM。</li>
                        <li><strong>可解释性：</strong> RAG系统通常可以指出答案所依据的源文档，提高了答案的可信度和可追溯性。</li>
                    </ul>

                    <h3>RAG问答机器人的核心组件</h3>
                    <p>构建一个RAG问答机器人通常涉及以下Langchain组件（许多在<a href="../chapter7/chapter7_indexes.html">第七章：Indexes模块</a>中已有介绍）：</p>
                    <ol>
                        <li><strong>文档加载器 (Document Loaders):</strong> 用于从各种来源（如文本文件、PDF、网页、数据库等）加载您的知识文档。示例中，我们使用 <code>TextLoader</code> 加载本地文本文件。</li>
                        <li><strong>文本分割器 (Text Splitters):</strong> 将加载的文档分割成较小的、语义相关的块（chunks）。这对于嵌入和检索的效率和效果至关重要。</li>
                        <li><strong>文本嵌入模型 (Text Embedding Models):</strong> 将文本块转换为数值向量（嵌入），捕捉其语义含义。示例中，我们通过自定义一个 Langchain <code>Embeddings</code> 类 (<code>QwenDashScopeEmbeddings</code>)，直接使用阿里云 DashScope SDK 来调用通义千问的文本嵌入服务 (如 <code>text-embedding-v2</code>)。</li>
                        <li><strong>向量存储 (Vector Stores):</strong> 存储嵌入向量并支持高效的相似性搜索。常见的有FAISS, Chroma, Pinecone等。示例中使用了 FAISS，并且向量存储在内存中，每次运行脚本时重新构建。</li>
                        <li><strong>检索器 (Retrievers):</strong> 根据用户的问题（查询），从向量存储中检索最相关的文本块。</li>
                        <li><strong>提示词模板 (Prompt Templates):</strong> 构建向LLM提问的提示词，通常包含检索到的上下文信息和用户的问题。</li>
                        <li><strong>大型语言模型 (LLM):</strong> 基于提供的上下文和问题生成最终答案。示例中使用的是通过 DashScope 兼容 OpenAI API 方式接入的 Qwen 模型 (如 <code>qwen-plus</code>)。</li>
                        <li><strong>链 (Chains):</strong> 将上述所有组件按特定顺序组合起来，形成一个完整的RAG流程。Langchain提供了如 <code>RetrievalQA</code> 这样的便捷链。</li>
                    </ol>

                    <h3>构建步骤概览</h3>
                    <p>以下是构建一个基本RAG问答机器人的典型步骤（与更新后代码示例对应）：</p>
                    <ol>
                        <li><strong>准备知识库：</strong> 准备一个名为 `data.txt` 的 UTF-8 编码文本文件作为知识库。如果此文件不存在，示例脚本会自动创建一个包含示例内容的文件。</li>
                        <li><strong>配置API密钥：</strong> 在脚本同目录下创建一个 `.env` 文件，并填入您的 `DASHSCOPE_API_KEY`。</li>
                        <li><strong>加载文档：</strong> 使用Langchain的文档加载器（如 `TextLoader`）加载 `data.txt` 文件。</li>
                        <li><strong>分割文档：</strong> 将加载的文档分割成合适的文本块。</li>
                        <li><strong>生成嵌入并构建向量存储：</strong> 使用自定义的通义千问嵌入模型（通过 DashScope SDK）为每个文本块生成嵌入，并将它们存储在内存中的 FAISS 向量数据库中。此示例中的向量数据库不会持久化到磁盘。</li>
                        <li><strong>创建检索器：</strong> 配置一个检索器，使其能够从向量数据库中根据查询检索相关文档块。</li>
                        <li><strong>构建问答链：</strong>
                            <ul>
                                <li>定义一个提示词模板，该模板将接收检索到的上下文和用户问题。</li>
                                <li>选择并初始化一个LLM（如 Qwen 系列模型）。</li>
                                <li>使用Langchain的 <code>RetrievalQA</code> 链将检索器、提示词模板和LLM组合起来。</li>
                            </ul>
                        </li>
                        <li><strong>交互式提问与获取答案：</strong> 用户通过命令行输入问题，链执行RAG流程（检索 -> 构建提示词 -> LLM生成答案），并返回结果。用户可以持续提问，直到输入“退出”。</li>
                    </ol>

                    <h3>示例场景：公司内部知识库问答机器人 (使用 Qwen 模型)</h3>
                    <p>假设我们希望构建一个问答机器人，它能基于一个本地文本文件 (`data.txt`) 中的内容来回答问题。本示例将使用阿里云的 DashScope 服务接入 Qwen 系列模型，并通过其 Python SDK 实现文本嵌入，LLM 则通过 DashScope 提供的兼容 OpenAI 的 API 接口接入。</p>

                    <p><strong>实现思路 (Python + Langchain + DashScope SDK + Qwen):</strong></p>
                    <div class="code-block">
<pre><code class="language-python">
import os
from dotenv import load_dotenv
from typing import List, Any

from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI  # Qwen LLM via OpenAI compatible API
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain_core.embeddings import Embeddings  # Base class for custom embeddings

# 尝试导入 dashscope，如果失败则提示安装
try:
    import dashscope
    from http import HTTPStatus
except ImportError:
    print("错误: 'dashscope' 包未找到。请运行 'pip install dashscope' 来安装它。")
    print("程序将退出。")
    exit()


# 1. 加载环境变量 (API Key)
def load_api_key():
    """加载 DashScope API Key 从 .env 文件"""
    load_dotenv()
    api_key = os.environ.get("DASHSCOPE_API_KEY")
    if not api_key:
        raise ValueError(
            "DASHSCOPE_API_KEY not found. Please set it in your .env file. "
            "You can get one from https://dashscope.console.aliyun.com/apiKey"
        )
    return api_key


# 2. 初始化通义千问大语言模型 (LLM)
def get_qwen_llm(api_key: str):
    """
    初始化并返回一个通义千问大语言模型实例。
    使用与 OpenAI API 兼容的接口。
    """
    qwen_llm = ChatOpenAI(
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
        api_key=api_key,
        model_name="qwen-plus", # 您可以根据需要更改为其他 qwen 模型，如 qwen-turbo, qwen-max
        temperature=0.7,
        streaming=False,
    )
    print("通义千问 LLM (qwen-plus) 初始化成功。")
    return qwen_llm


# 3. 自定义的通义千问文本嵌入模型 (使用 DashScope SDK)
class QwenDashScopeEmbeddings(Embeddings):
    """
    Custom Langchain Embeddings class for Qwen models using DashScope SDK.
    """

    def __init__(self, api_key: str, model_name: str = dashscope.TextEmbedding.Models.text_embedding_v2):
        if not api_key:
            raise ValueError("DashScope API Key cannot be empty.")
        dashscope.api_key = api_key  # 设置全局 API Key for dashscope SDK
        self.model = model_name
        # text-embedding-v2 限制：单次请求的输入文本数量不超过25个，总token数不超过2048*25
        # 单个文本长度限制 2048 tokens
        self.batch_size = 25  # DashScope API limit for number of texts per call

    def _embed_batch(self, texts_batch: List[str]) -> List[List[float]]:
        """Embeds a single batch of texts."""
        if not texts_batch:
            return []

        # 确保批次大小不超过API限制 (虽然外部循环已经处理，这里作为双重检查)
        if len(texts_batch) > self.batch_size:
            # This case should ideally be handled by the calling loop in embed_documents
            raise ValueError(f"Batch size {len(texts_batch)} exceeds DashScope limit of {self.batch_size}")

        try:
            resp = dashscope.TextEmbedding.call(
                model=self.model,
                input=texts_batch  # DashScope SDK handles list of strings directly
            )
            if resp.status_code == HTTPStatus.OK:
                # 确保返回的嵌入向量顺序与输入文本顺序一致
                # DashScope SDK 返回的 embeddings 列表与输入 texts 列表顺序一致
                return [embedding['embedding'] for embedding in resp.output['embeddings']]
            else:
                error_message = f"DashScope API Error for embeddings: {resp.code} - {resp.message}. Request ID: {resp.request_id}"
                print(error_message)
                # 根据错误类型决定是否重试或抛出更具体的异常
                # 为简单起见，这里直接抛出异常
                raise ValueError(error_message)
        except Exception as e:
            # 捕获SDK调用或网络问题
            sdk_error_message = f"Exception during DashScope SDK call for embeddings: {e}"
            print(sdk_error_message)
            raise RuntimeError(sdk_error_message) from e

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        """Embed a list of documents (texts)."""
        all_embeddings: List[List[float]] = []
        if not texts:
            return all_embeddings

        for i in range(0, len(texts), self.batch_size):
            batch = texts[i:i + self.batch_size]
            if not batch:  # Should not happen if texts is not empty
                continue
            try:
                batch_embeddings = self._embed_batch(batch)
                all_embeddings.extend(batch_embeddings)
            except Exception as e:
                # 如果一个批次失败，可以选择停止并抛出异常，或者记录错误并继续（可能导致部分嵌入）
                # 这里选择停止并抛出，因为对于RAG，所有文档都嵌入成功很重要
                print(f"Failed to embed batch starting at index {i}: {e}")
                raise  # Re-throw the exception to stop the process

        return all_embeddings

    def embed_query(self, text: str) -> List[float]:
        """Embed a single query text."""
        if not text:
            # DashScope 可能不允许空字符串嵌入，或者返回特定错误
            # 为保持一致性，可以返回一个零向量或抛出错误
            # 这里我们尝试发送，让API决定
            print("Warning: Attempting to embed an empty query string.")

        try:
            resp = dashscope.TextEmbedding.call(
                model=self.model,
                input=text  # Single string
            )
            if resp.status_code == HTTPStatus.OK:
                if resp.output and resp.output.get('embeddings') and len(resp.output['embeddings']) > 0:
                    return resp.output['embeddings'][0]['embedding']
                else:
                    raise ValueError("DashScope API returned OK but no embeddings found for query.")
            else:
                error_message = f"DashScope API Error for query embedding: {resp.code} - {resp.message}. Request ID: {resp.request_id}"
                print(error_message)
                raise ValueError(error_message)
        except Exception as e:
            sdk_error_message = f"Exception during DashScope SDK call for query embedding: {e}"
            print(sdk_error_message)
            raise RuntimeError(sdk_error_message) from e


def get_qwen_custom_embeddings(api_key: str):
    """初始化并返回自定义的通义千问文本嵌入模型实例。"""
    # 默认使用 text-embedding-v2 模型
    embeddings = QwenDashScopeEmbeddings(api_key=api_key, model_name=dashscope.TextEmbedding.Models.text_embedding_v2)
    print(f"自定义 Qwen DashScope Embeddings ({dashscope.TextEmbedding.Models.text_embedding_v2}) 初始化成功。")
    return embeddings


def main():
    """主函数，运行RAG问答机器人"""
    data_file_path = "data.txt" # 定义知识库文件名

    # 检查知识库文件是否存在，不存在则创建一个示例文件
    if not os.path.exists(data_file_path):
        print(f"警告: 知识库文件 '{data_file_path}' 未找到。")
        try:
            with open(data_file_path, "w", encoding="utf-8") as f:
                f.write("这是data.txt中的第一行示例内容：Langchain和Qwen是强大的工具。\n")
                f.write("这是第二行：RAG技术可以提高问答系统的准确性。\n")
                f.write("通义千问支持多种尺寸的模型，如qwen-plus, qwen-turbo等。\n")
                f.write("自定义嵌入模型可以使用DashScope SDK调用text-embedding-v2。\n")
            print(f"已创建示例知识库文件 '{data_file_path}'。请根据您的需求修改其内容。")
        except IOError as e:
            print(f"创建示例文件 '{data_file_path}' 失败: {e}")
            return

    try:
        # 加载API Key
        api_key = load_api_key()
        
        # 初始化LLM
        llm = get_qwen_llm(api_key)
        
        # 初始化自定义的Embedding模型
        embeddings_model = get_qwen_custom_embeddings(api_key)

        print(f"\n[步骤 1/5] 正在从 '{data_file_path}' 加载文档...")
        loader = TextLoader(data_file_path, encoding="utf-8")
        documents = loader.load()
        if not documents:
            print(f"错误: 从 '{data_file_path}' 加载的文档为空。请检查文件内容。")
            return
        print(f"文档加载完成，共 {len(documents)} 个文档。")

        print("\n[步骤 2/5] 正在分割文档...")
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500, # 每个块的最大字符数
            chunk_overlap=50  # 块之间的重叠字符数
        )
        texts_docs = text_splitter.split_documents(documents)  # 返回 List[Document]
        if not texts_docs:
            print("错误: 文档分割后没有生成任何文本块。请检查文档内容和分割参数。")
            return
        print(f"文档分割完成，共 {len(texts_docs)} 个文本块。")

        print("\n[步骤 3/5] 正在创建向量存储 (FAISS) (内存中)...")
        # FAISS.from_documents 会从 Document 对象中提取 page_content 并使用提供的嵌入模型
        vector_store = FAISS.from_documents(texts_docs, embeddings_model)
        print("向量存储创建完成。")

        # 创建检索器，从向量存储中检索k个最相关的文档块
        retriever = vector_store.as_retriever(search_kwargs={"k": 3})
        print("\n[步骤 4/5] 检索器创建完成 (检索 top 3 相关文本块)。")

        # 定义提示模板
        prompt_template = """
        请仅根据下面提供的上下文信息来回答问题。
        如果你在上下文中找不到答案，请明确说明你不知道，不要试图编造答案。
        请保持答案简洁明了。

        上下文:
        {context}

        问题: {question}

        有用的回答:"""

        QA_CHAIN_PROMPT = PromptTemplate(
            input_variables=["context", "question"],
            template=prompt_template,
        )

        # 创建RetrievalQA链
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff", # "stuff"是最简单的方式，将所有检索到的文本直接塞给LLM
            retriever=retriever,
            return_source_documents=True, # 同时返回源文档片段
            chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
        )
        print("\n[步骤 5/5] RAG 问答链构建完成。")

        print("\n问答机器人已准备就绪！请输入您的问题。输入 '退出' 来结束程序。")
        while True:
            user_query = input("\n您的问题: ")
            if user_query.strip().lower() == '退出':
                print("感谢使用，再见！")
                break
            if not user_query.strip():
                print("请输入有效的问题。")
                continue

            print("\n正在思考...")
            try:
                result = qa_chain.invoke({"query": user_query})

                print("\n答案:")
                print(result["result"])

                if result.get("source_documents"):
                    print("\n参考的文档片段:")
                    for i, doc in enumerate(result["source_documents"]):
                        print(f"片段 {i + 1}:")
                        # Document 对象有 page_content 和 metadata 属性
                        print(f"  内容: {doc.page_content[:200]}...") # 打印片段前200字符
                        if doc.metadata and 'source' in doc.metadata:
                            print(f"  来源: {doc.metadata['source']}")
            except Exception as e:
                print(f"处理您的问题时发生错误: {e}")
                import traceback
                traceback.print_exc()  # 打印详细的错误堆栈
            print("-" * 40)

    except ValueError as ve:
        print(f"配置或参数错误: {ve}")
    except RuntimeError as re:  # 自定义Embedding类中可能抛出RuntimeError
        print(f"运行时错误 (可能来自SDK调用): {re}")
    except Exception as e:
        print(f"发生意外错误: {e}")
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    main()
</code></pre>
                    </div>
                    <p class="note">
                        <strong>注意:</strong> 上述代码展示了如何使用 Langchain 结合阿里云 DashScope SDK 构建一个基于 Qwen 模型的 RAG 问答机器人。
                        关键特性包括：
                        <ul>
                            <li>使用 <code>python-dotenv</code> 管理 <code>DASHSCOPE_API_KEY</code>（存放于项目根目录下的 <code>.env</code> 文件）。</li>
                            <li>通过自定义的 <code>QwenDashScopeEmbeddings</code> 类直接调用 DashScope 的文本嵌入服务（例如 <code>text-embedding-v2</code>）。该类处理了与 DashScope API 的直接交互和批处理。</li>
                            <li>LLM 使用与 OpenAI 兼容接口的 Qwen 模型（例如 <code>qwen-plus</code>），通过 DashScope 的兼容模式接入。</li>
                            <li>知识库从本地一个名为 <code>data.txt</code> 的 UTF-8 编码文本文件加载。如果该文件不存在，脚本会尝试创建一个包含示例内容的文件。</li>
                            <li>向量索引 (FAISS) 在内存中构建，程序每次运行时都会重新生成，退出后不保留。如果需要持久化索引，可以研究FAISS的 <code>save_local</code> 和 <code>load_local</code> 方法。</li>
                            <li>脚本提供了一个交互式的命令行界面，用户可以连续提问，直到输入“退出”。</li>
                        </ul>

                        <strong>运行前准备：</strong>
                        <ol>
                            <li><strong>安装必要的 Python 库：</strong>
                                <br>确保已安装 <code>langchain</code>, <code>langchain-community</code>, <code>langchain-openai</code>, <code>faiss-cpu</code> (或 <code>faiss-gpu</code> 如果您有NVIDIA GPU并希望使用它), <code>dashscope</code>, <code>python-dotenv</code>, 和 <code>tiktoken</code>。
                                <br>您可以使用 pip 安装它们：
                                <pre><code>pip install langchain langchain-community langchain-openai faiss-cpu dashscope python-dotenv tiktoken</code></pre>
                            </li>
                            <li><strong>配置 DashScope API Key：</strong>
                                <br>在您的 Python 脚本所在的目录创建一个名为 <code>.env</code> 的文件。
                                <br>在该 <code>.env</code> 文件中，添加您的 DashScope API Key，格式如下：
                                <pre><code>DASHSCOPE_API_KEY="sk-your_actual_api_key"</code></pre>
                                将 <code>sk-your_actual_api_key</code> 替换为您从<a href="https://dashscope.console.aliyun.com/apiKey" target="_blank">阿里云DashScope控制台</a>获取的真实 API Key。
                            </li>
                            <li><strong>准备知识库文件：</strong>
                                <br>在脚本同目录下准备一个名为 <code>data.txt</code> 的 UTF-8 编码的文本文件。这个文件将作为机器人的知识来源。您可以放入任何您希望机器人能够回答的文本内容。如果脚本运行时未找到此文件，它会尝试创建一个包含几行示例内容的 <code>data.txt</code> 文件。
                            </li>
                        </ol>
                        请将上述 Python 代码复制到一个 <code>.py</code> 文件（例如 <code>rag_qwen_bot.py</code>）中，并在配置好环境和 <code>.env</code> 文件后，通过 Python 解释器运行该脚本。
                    </p>

                    <h3>关键考虑因素和进阶技巧</h3>
                    <ul>
                        <li><strong>文档块大小 (Chunk Size) 和重叠 (Overlap):</strong> 需要仔细调整。太小可能丢失上下文，太大可能包含过多无关信息或超出模型上下文窗口。<code>RecursiveCharacterTextSplitter</code> 提供了基于字符的分割方法。</li>
                        <li><strong>检索策略:</strong> 除了基本的相似性搜索（示例中检索top-k），还可以使用更高级的检索策略，如MMR (Maximal Marginal Relevance) 来增加结果的多样性。这些可以通过配置传递给 <code>as_retriever</code> 方法的 <code>search_type</code> 和 <code>search_kwargs</code> 参数来调整。</li>
                        <li><strong>上下文管理:</strong> 如何将检索到的多个文档块有效地组合并传递给LLM。<code>stuff</code>是最简单的方法，但对于大量文档块可能不适用（可能超出LLM的上下文长度限制）。<code>map_reduce</code>, <code>refine</code>, <code>map_rerank</code>是处理更多上下文的策略，可以在创建<code>RetrievalQA</code>链时通过<code>chain_type</code>参数指定。</li>
                        <li><strong>提示工程:</strong> 设计有效的提示词对于引导LLM基于检索到的上下文生成高质量答案至关重要。示例中的模板鼓励模型基于上下文回答，并在信息不足时承认。</li>
                        <li><strong>评估:</strong> 评估RAG系统的性能可能比较复杂，需要考虑检索的准确性（召回率、精确率）和生成的答案质量（相关性、流畅性、真实性）。Langchain也提供了一些评估工具和框架。</li>
                        <li><strong>成本和延迟:</strong> 嵌入生成（调用DashScope Embedding API）、向量存储查询（本地FAISS操作通常较快）和LLM调用（调用DashScope LLM API）都会产生费用和延迟，需要进行优化和监控。</li>
                        <li><strong>混合搜索:</strong> 结合向量搜索和传统的基于关键词的搜索（如BM25）有时可以提高检索效果，特别是在处理特定术语或精确匹配查询时。</li>
                        <li><strong>重新排序 (Re-ranking):</strong> 在检索步骤之后，可以使用一个更小的、专门的模型（或更复杂的逻辑）对检索到的结果进行重新排序，以提高最相关文档块的排名，然后再将其传递给LLM。</li>
                        <li><strong>索引持久化与更新：</strong> 示例代码每次运行时在内存中重建FAISS索引。对于大型知识库或需要持久化状态的应用，应将索引保存到磁盘 (使用 <code>vectorstore.save_local()</code>) 并在后续运行时加载 (使用 <code>FAISS.load_local()</code>)。当知识库更新时，需要考虑如何有效地更新索引（完全重建或增量更新，取决于向量数据库的支持）。</li>
                    </ul>

                    <p>通过结合Langchain的强大组件和如Qwen这样的先进模型，您可以构建出高效、准确且能处理特定领域知识的RAG问答机器人，极大地扩展LLM的应用潜力。</p>
                </div>

            </section>
        </main>
    </div>
        <div class="navigation">
            <a href="../langchain_tutorial_outline.html">返回教程目录</a>
            <a href="../chapter9/chapter9_application_role_playing_ai.html">上一节：9.5 如何利用Langchain和提示词构建具有特定角色的AI应用（如客服助手、编程助手）</a>
            <!-- <a href="../chapter10/chapter10_prompt_engineering_advanced.html">下一章：AI系统提示词设计进阶</a> -->
        </div>
</body>
</html>
